
# Наследование  (4 ак. ч.)

* Принцип утиной типизации.
* Понятия базового класса и производного класса.
* Множественное наследование
* Миксины и слоты
* Практикум

### Утиная типизация
Часто, когда речь заходит о Python, всплывает фраза утиная типизация, или даже что-нибудь вроде:

> Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка

Утиная типизация – это концепция, характерная для языков программирования с динамической типизацией, согласно которой конкретный тип или класс объекта не важен, а важны лишь свойства и методы, которыми этот объект обладает. 

Другими словами, при работе с объектом его тип не проверяется, вместо этого проверяются свойства и методы этого объекта.

Такой подход добавляет гибкости коду, позволяет полиморфно работать с объектами, которые никак не связаны друг с другом и могут быть объектами разных классов. 

Единственное условие, чтобы все эти объекты поддерживали необходимый набор свойств и методов.

```python
class Meter:
    def __len__(self):
        return 1_000

len([1, 2, 3])

len("Duck typing...")

print(len(Meter()))
```


В примере выше функции `len` не важен тип аргумента, а важно лишь то, что у объекта можно вызвать метод `__len__()`.

Но именно эта гибкость и усложняет раннее обнаружение ошибок типизации. 

Корректность использования объектов определяется динамически, в момент выполнения программы, и зачастую тестирование – единственный способ отловить подобные ошибки. 

Статическая проверка типов и корректности программы в данном случае представляет значительную сложность.

`
Утиная типизация заключается в том, что вместо проверки типа чего-либо в `Python` мы склонны проверять, какое поведение оно поддерживает, зачастую пытаясь использовать это поведение и перехватывая исключение, если оно не работает.
`

Python программисты говорят `если это похоже на утку и крякает как утка, то это утка`. 

Не нужно проверять ДНК утки, чтобы понять утка ли это, нужно просто посмотреть на ее поведение.

Утиная типизация `DuckTyping` настолько глубоко заложена и распространена в Python, что она действительно повсюду, как вода для рыбы: мы даже не думаем об этом. 

В Python очень часто проще предположить поведение объектов, вместо проверки их типов.

Идея утиной типизации `Duck Typing` в языке программирования Python повсеместна.

Встроенная функция `sum()` принимает любые повторяющиеся объекты, которые она может сложить вместе. 

То есть она работает со всем, что поддерживает знак '+', даже со списками и кортежами:

> sum([(1, 2), (3, 4)], ())

Встроенные функции zip и enumerate принимают любые iterable - итерируемые объекты. 

Не только list или sequence, любые iterable!

> list(zip([1, 2, 3], (4, 5, 6), range(3)))




## Супер-класс и класс-наследник

```python
class СуперКласс(object):
    ...


class КлассНаследник(СуперКласс):
    ...


объект_супер = СуперКласс()
объект = КлассНаследник()

isinstance(объект, КлассНаследник)
isinstance(объект, СуперКласс)
issubclass(КлассНаследник, СуперКласс)
```


Если мы возьмем классы, которые мы рассматривали. К примеру, что общего у списка, кортежа, строки? 

Они иттерабельны. У них есть длина – метод `len`.  

Но все остальное у них разное. Методы.

Смысл в следующем – одно и тоже глупо описывать несколько раз. 

В этом деле нам помогает наследование. 


Все классы состоят в некой иерархии. 

СуперКласс `Животные`, в нем подкласс `Млекопитающие`, `Земноводные`, `Птицы`, `Рыб` и т.п.

`Млекопитающие` делятся дальше `живородящие` и однопроходные. 

`Живородящие` на `низшие` и `высшие`.

Тоже самое в Пайтоне. 

Есть некий класс `object` от которого происходят (или наследуются) все остальные классы.

Но мы на этом не останавливаемся. Сделаем заготовку для изучения второго принципа ООП.

```python

class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Я точка: {self.x} x {self.y}'

class Point3D(Point):

    def __init__(self, x, y, z):
        self.z = z
        super().__init__(x, y)

    def __repr__(self):
        s = super().__repr__()
        return f'{s} x {self.z}'


p = Point(1, 2)
print(p)
p3d = Point3D(10, 20, 30)
print(p3d)
```

> Что делает метод супер?

```python
class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y
        Point.__count += 1

    def move_to(self, x, y):
        self.x = x
        self.y = y



    def __repr__(self):
        return f'Я точка: {self.x} x {self.y}'
```

На самоме деле наш класс унаследован от того самого object

```python
class Point(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y
```
посмотрим на его начинку

```python
pprint(object.__dict__)
```

Т.е. класс `object` – это Адам и Ева в одном флаконе. 

Это `Суперкласс`, в котором уже описаны базовые свойственные другим классам Методы

Там есть и `__repr__и __str__ и __setattr__ и __init__ `и много чего интересного.

Это означает, что наш класс `Point` получает по наследству всё, что там написано. 

Все этим магические, волшебные, дандер методы, которые там описаны.


И теперь с помощью функции `isinstance` мы можем вычислить всех родителей данного объекта.
Т.е. и `СуперКласс` и `КлассНаследник` являются родителями нашего объекта

Видим `True`, в т.ч. в цепочке наследования.

Является ли кошка экземпляром класса `Кошачьи` – да, а экземпляром класс `Животные`. 
Тоже – да.
А может быть экземпляром класса `Домашние животные`. 

Да, легко.
Чтобы определить принадлежность класса подклассу используется функция `issubclass`.

```python
issubclass(Point, object)
Давайте создадим 
class Point3D(Point):
    pass
```

и создадим экземпляр этого класса

```python
p3d = Point3D(10,20)
print(p3d.x, p3d.y)
print (p3d)
```
Вы видим, что объект класса `Point3D` полностью отнаследовал всё свойства и методы класса `Point`.
Как это произошло?
При вызове конструктора он пошел в класс `Point3D`, там он инициализатор `init` не нашел, поэтому потом он полез в родительский класс `Point3D` и успешно проинициализировался. 

Тоже самое произошло и с остальными методами.

Покажи мне x и y. 

Покажи мне `repr`. 

А этажем выше есть. 

Давайте уберем `repr`.

Позовем `isinstance` и `issubclass` для проверки. 
 
А теперь добавим уникальные метод для Point3dD

```python
class Point3D(Point):
    def move_by(self, x, y):
        self.x += x
        self.y += y
```
Теперь `Point3D` имеет допонительно метод `move_by`
Попробуем вызвать метод `move_by` у двумерной точки

```python
p = Point(1,2)

p3d = Point3D(10, 20)

p.move_to(100,200)

p.move_by(100,200)

p3d.move_by(300,400)

```

видим ошибку, т.к. `move_by` у `point` нет.

Т.е мы просто расширили поведение базового класса. 
* Это первое. Расширение.

* Второе, что мы можем сделать – это переопределить метод базового класса.

```python
def __repr__(self):
    return f'Я 3D точка: {self.x} x {self.y} x {self.z}'
```

Но у нас же `3d-точка`.

Как нам это сделать. У нас же нет конструктора для `3d-точки`. 

Автоматом вызывается первый конструктор. Но в нем 2 параметра, а мы хотим передать три.

Самый простой способ – переопределить конструктор. 
```python
def __init__(self, x, y , z):
    self.x = x
    self.y = y
    self.z = z
```
и скорректировать метод `move_by`

```python
def move_by(self, x, y, z):
    self.x += x
    self.y += y
    self.z += z
```

Но, однако неудобно, тупо переписано? 

скопировано. В чем засада? 
А если мы хотим добавит точке четвертое измерение. Мы же не будем глупо копипастить.

Как быть, что пробрасывать, а что дописать. 

Подходим к понятию конструктор суперкласса

```python
class Point3D(Point):

    def __init__(self, x, y, z):
        self.z = z
        Point.__init__(self, x, y)
```

т.о. мы переопредели конструктор, точнее его всего один параметр. 

Теперь из производного класса мы вызовем родительский конструктор, в который пробрасываем два параметра `x и y`.

Всё работает отлично, ничего не сломалось, ошибок нет. 
Т.е. мы все лишь дописали нужную нам часть.

Оптимизируем таким же образом метод `__repr__`

```python
def __repr__(self):
    s = Point.__repr__(self)
    return f'{s} x {self.z}'

Тоже самое с move_to
def move_to(self, x,y, z):
    self.z = z
    Point.move_to(self,x,y)

```

Однако, совершенству нет предела.

Каждый раз мы явно пишем имя класс `Point`. 
А теперь будем использовать волшебное слово `super` ддя обращения к родительскому (или супер-классу). 

```python
def move_to(self, x,y, z):
    self.z = z
    # Point.move_to(self,x,y)
    super().move_to(x,y)
```

Что-то я забыл ? self ? 

Он здесь не нужен! 

Хотя технически мы можем его туда передать таким образом
```python
super(Point3D,self).move_to(x,y)
```

у любого объекта есть магический (волшебный атрибут) `__class__ `

`p3d.__class__`

`<class '__main__.Point3D'>`

Который возвращает родительский класс
А у волшебного метода `__class__` имеется еще более волшебный метод `__name__`

`p3d.__class__.__name__`
> 'Point3D'

`p.__class__.__name__`
> Point'
 

`name` дает имя.

Исходя из этого мы можем снова переписать метод `move_to`

```python
#super().move_to(x, y)
#super(Point3D,self).move_to(x,y)
super(self.__class__, self).move_to(x, y)
```

Это всё одно и тоже



В конечном итоге наш класс Point3D мы должны переписать с использованием волшебных супер-методов

```python
class Point3D(Point):

    def __init__(self, x, y, z):
        self.z = z
        super().__init__(x, y)

    def move_to(self, x,y, z):
        self.z = z
        super().move_to(x, y)


    def __repr__(self):
        s = super().__repr__()
        return f'{s} x {self.z}'

```



### Добавление методов. Расширение класса методами

```python
class СуперКласс:
    def метод1(self):
        pass

    def метод2(self):
        pass


class КлассНаследник(СуперКласс):
    def метод(self):
        pass


объект = КлассНаследник()

объект.метод()
объект.метод1()
объект.метод2()
```


### Переопределение метода

```python
class СуперКласс:
    def метод1(self):
        pass

    def метод2(self):
        pass


class КлассНаследник(СуперКласс):
    def метод2(self, параметр):
        pass


объект = КлассНаследник()

объект.метод1()
объект.метод2('значение')

```

### Обращение к методу суперкласса

```python
class СуперКласс:
    def метод(self, параметр):
        pass


class КлассНаследник(СуперКласс):
    def метод(self, параметр1, параметр2):
        СуперКласс.метод2(self, параметр1)


объект = КлассНаследник()

объект.метод('значение1', 'значение2')


class КлассНаследник(СуперКласс):
    def метод(self, параметр1, параметр2):
        super().метод(параметр1)  # Ого! никакого self!

```

